跳到主要内容

创建型模式-原型模式

原型模式是什么?

主要知识点就是 浅表复制和深层复制

原型模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。例如,一个对象需要在一个高代价的数据库操作之后被创建。可以直接缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。

classDiagram Client --> Prototype Prototype <|-- ConcretePrototype Prototype : <<abstract>> Prototype : +Clone() ConcretePrototype : +Clone()

浅表复制(Java)

浅表复制,被复制对象的所有变量都含有与原来的对象相同的值,而所有的 对其它对象的引用都指向原来的对象。所以当内部有引用类型的时候就需要使用深复制

问题1:String 类型到底属于浅复制还是深复制?

一般的 clone() 方法对对象中的 String 的克隆应该属于深克隆。原因在于 String 对象的引用所指向的地址的内容是不可变的,比如 String str = "java";,除非使用反射,否则无法改变 “java” 任何一个字符(效果上这是相当于实现了深复制)

问题2:Integer 这种包装类是值类型还是引用类型?

实际上 Integer 这种包装类和 String 一样被 final 修饰了,所以效果和 String 是一样的,都是可以直接作为深复制 public final class Integer extends Number....

使用示例: Java 的 Cloneable (可克隆) 接口就是立即可用的原型模式。

public class ConcretePrototype implements Cloneable {

private String name;
private Integer age;

public ConcretePrototype(String name, Integer age) {
this.name = name;
this.age = age;
}

// 必须重写这个 clone() 方法它才会生效(clone 方法执行的是浅拷贝)
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}

// 因为默认的 equals 比较的是地址,复制之后地址是不一样的,所以需要重写一下 equals 方法
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ConcretePrototype1 that = (ConcretePrototype1) o;
return Objects.equals(name, that.name) &&
Objects.equals(age, that.age);
}

@Override
public int hashCode() {
return Objects.hash(name, age);
}

// 省略 Getter、Setter 和 toString ...
}

克隆这个实体

public class Client {
// 会抛一个异常
public static void main(String[] args) throws CloneNotSupportedException {
ConcretePrototype c1 = new ConcretePrototype("张三", 18);
ConcretePrototype c2 = (ConcretePrototype) c1.clone();
System.out.println(c2);
// 返回是 true
System.out.println(c1.equals(c2));
}
}

深层复制

使用 clone 接口

方式一:使用 clone 方法(需要实现 Cloneable 接口)

接着上面的浅表复制的代码,重写那个 clone() 方法

@Override
protected Object clone() throws CloneNotSupportedException {
ConcretePrototype1 c = (ConcretePrototype1) super.clone();
// 如果要使 ConcretePrototype 对象在clone时进行深拷贝,
// 那么就要在 ConcretePrototype 的 clone 方法中,将源对象引用的 Date 对象也 clone 一份。
c.birthday = (Date) c.birthday.clone();
return c;
}

如上,如果内部的引用类型里面还有别的引用类型,要复制也是一样的操作,一个一个处理,但是这样的方式面对的是一个复杂的对象要彻底的深拷贝其实是很困难的(例如里面调用了一堆第三方对象)

使用序列化的方式

方法二:通过对象的序列化实现(需要实现 Serializable 接口),这种方式才是真正意义上的的深度克隆

public class DeepProtoType implements Serializable, Cloneable {
private String name;
private DeepCloneableTarget deepCloneableTarget;

// get set 方法这里就不写了

// 通过对象的序列化实现(推荐)
public Object deepClone() {
//创建流对象
ByteArrayOutputStream bos = null;
ObjectOutputStream oos = null;
ByteArrayInputStream bis = null;
ObjectInputStream ois = null;
try {
//序列化
bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
oos.writeObject(this);
//反序列化
bis = new ByteArrayInputStream(bos.toByteArray());
ois = new ObjectInputStream(bis);
DeepProtoType o = (DeepProtoType) ois.readObject();
return o;

} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
try {
bos.close();
oos.close();
bis.close();
ois.close();
} catch (Exception e2) {
System.out.println(e2.getMessage());
}
}
}
}

客户端代码, 使用写好的 deepClone 方法复制对象

public static void main(String[] args) throws CloneNotSupportedException {
DeepProtoType p = new DeepProtoType();
p.setName("小明");
p.setDeepCloneableTarget(new DeepCloneableTarget("小红"));

// 完成深拷贝
DeepProtoType p2 = (DeepProtoType) p.deepClone();
System.out.println(p.getDeepCloneableTarget().hashCode()); // 输出结果 621009875
System.out.println(p2.getDeepCloneableTarget().hashCode()); // 输出结果 2065951873
}

JSON 的深层复制

实际上这个方式实现的深层复制有点像 JavaScript的那种转成 JSON 再转成对象的方式

var array = [
    { number: 1 },
    { number: 2 },
    { number: 3 }
];

var copyArray = JSON.parse(JSON.stringify(array))
copyArray[0].number = 100;
console.log(array); //  [{number: 1}, { number: 2 }, { number: 3 }]
console.log(copyArray); // [{number: 100}, { number: 2 }, { number: 3 }]

游戏中的原型模式

下半部分转载自 游戏设计模式Design Patterns Revisited

假设我们要用《圣铠传说》的风格做款游戏。 野兽和恶魔围绕着英雄,争着要吃他的血肉。 这些可怖的同行者通过 “生产者” 进入这片区域,每种敌人有不同的生产者。

在这个例子中,假设我们游戏中每种怪物都有不同的类——Ghost,Demon,Sorcerer等等,像这样:

class Monster
{
// 代码……
};

class Ghost : public Monster {};
class Demon : public Monster {};
class Sorcerer : public Monster {};

生产者构造特定种类怪物的实例。 为了在游戏中支持每种怪物,我们可以用一种暴力的实现方法, 让每个怪物类都有生产者类,得到平行的类结构:

class Spawner
{
public:
virtual ~Spawner() {}
virtual Monster* spawnMonster() = 0;
};

class GhostSpawner : public Spawner
{
public:
virtual Monster* spawnMonster()
{
return new Ghost();
}
};

class DemonSpawner : public Spawner
{
public:
virtual Monster* spawnMonster()
{
return new Demon();
}
};

// 下略……

除非你会根据代码量来获得工资, 否则将这些焊在一起很明显不是好方法(指每种怪兽就一个 “生产者” 类)。 众多类,众多引用,众多冗余,众多副本,众多重复自我……

原型模式提供了一个解决方案。 关键思路是一个对象可以产出与它自己相近的对象。 如果你有一个恶灵,你可以制造更多恶灵。 如果你有一个恶魔,你可以制造其他恶魔。 任何怪物都可以被视为原型怪物,产出其他版本的自己。

为了实现这个功能,我们给基类 Monster 添加一个抽象方法 clone()

class Monster
{
public:
virtual ~Monster() {}
virtual Monster* clone() = 0;

// 其他代码……
};

每个怪兽子类提供一个特定实现,返回与它自己的类和状态都完全一样的新对象。举个例子:

class Ghost : public Monster {
public:
Ghost(int health, int speed)
: health_(health),
speed_(speed)
{}

virtual Monster* clone()
{
return new Ghost(health_, speed_);
}

private:
int health_;
int speed_;
};

一旦我们所有的怪物都支持这个, 我们不再需要为每个怪物类创建生产者类。我们只需定义一个类:

class Spawner
{
public:
Spawner(Monster* prototype)
: prototype_(prototype)
{}

Monster* spawnMonster()
{
return prototype_->clone();
}

private:
Monster* prototype_;
};

它内部存有一个怪物,一个隐藏的怪物, 它唯一的任务就是被生产者当做模板,去产生更多一样的怪物, 有点像一个从来不离开巢穴的蜂后。

为了得到恶灵生产者,我们创建一个恶灵的原型实例,然后创建拥有这个实例的生产者:

Monster* ghostPrototype = new Ghost(15, 3);
Spawner* ghostSpawner = new Spawner(ghostPrototype);

这个模式的灵巧之处在于它不但拷贝原型的类,也拷贝它的状态。 这就意味着我们可以创建一个生产者,生产快速鬼魂,虚弱鬼魂,慢速鬼魂,而只需创建一个合适的原型鬼魂。

游戏例子中的深层复制问题

当你坐下来试着写一个正确的 clone(),会遇见令人不快的语义漏洞。 做深层拷贝还是浅层拷贝呢?换言之,如果恶魔拿着叉子,克隆恶魔也要克隆叉子吗?

同时,这看上去没减少已存问题上的代码, 事实上还增添了些人为的问题。 我们需要将每个怪物有独立的类作为前提条件。 这绝对不是当今大多数游戏引擎运作的方法。

我们中大部分痛苦地学到,这样庞杂的类层次管理起来很痛苦, 那就是我们为什么用 组件模式类型对象 为不同的实体建模,这样 无需一一建构自己的类

使用回调函数创建

哪怕我们确实需要为每个怪物构建不同的类,这里还有其他的实现方法。 不是使用为每个怪物建立分离的生产者类,我们可以创建生产函数,就像这样:(就是传递回调函数)

Monster* spawnGhost()
{
return new Ghost();
}

这比构建怪兽生产者类更简洁。生产者类只需简单地存储一个函数指针:

typedef Monster* (*SpawnCallback)();

class Spawner
{
public:
Spawner(SpawnCallback spawn)
: spawn_(spawn)
{}

Monster* spawnMonster()
{
return spawn_();
}

private:
SpawnCallback spawn_;
};

为了给恶灵构建生产者,你需要做:

Spawner* ghostSpawner = new Spawner(spawnGhost);

使用模板(Java 的泛型)对上面的方法进行修改, 生产者类需要为某类怪物构建实例,但是我们不想硬编码是哪类怪物。 自然的解决方案是将它作为模板中的类型参数:

class Spawner
{
public:
virtual ~Spawner() {}
virtual Monster* spawnMonster() = 0;
};

template <class T>
class SpawnerFor : public Spawner
{
public:
virtual Monster* spawnMonster() { return new T(); }
};

像这样使用它:

Spawner* ghostSpawner = new SpawnerFor<Ghost>();

原型语言范式

很多人认为 “面向对象编程” 和 “类” 是同义词。 OOP 的定义却让人感觉正好相反, 毫无疑问,OOP 让你定义 “对象”,将数据和代码绑定在一起。 与C这样的结构化语言相比,与 Scheme 这样的函数语言相比, OOP的特性是它将状态和行为紧紧地绑在一起。

你也许认为类是完成这个的唯一方式方法, 但是包括 Dave Ungar 和 Randall Smith 的一大堆家伙一直在拼命区分OOP和类。 他们在80年代创建了一种叫做 Self 的语言。它不用类实现了 OOP。

Self 语言

就单纯意义而言,Self 比基于类的语言更加面向对象。 我们认为 OOP 将状态和行为绑在一起,但是基于类的语言实际将状态和行为割裂开来。

拿基于类的语言的语法来说。 为了接触对象中的一些状态,需要在实例的内存中查询。状态包含在实例中。

但是,为了调用方法,你需要找到实例的类, 然后在那里调用方法。行为包含在类中。 获得方法总需要通过中间层,这意味着字段和方法是不同的。

Self 结束了这种分歧。无论你要找啥,都只需在对象中找。 实例同时包含状态和行为。你可以构建拥有完全独特方法的对象。

如果这就是 Self 语言的全部,那它将很难使用。 基于类的语言中的继承,不管有多少缺陷,总归提供了有用的机制来重用代码,避免重复。 为了不使用类而实现一些类似的功能,Self 语言加入了委托。

如果要在对象中寻找字段或者调用方法,首先在对象内部查找。 如果能找到,那就成了。如果找不到,在对象的父对象中寻找。 这里的父类仅仅是一个对其他对象的引用。

当我们没能在第一个对象中找到属性,我们尝试它的父对象,然后父类的父对象,继续下去直到找到或者没有父对象为止。 换言之,失败的查找被委托给对象的父对象。

父对象让我们在不同对象间重用行为(还有状态!),这样就完成了类的公用功能。 类做的另一个关键事情就是给出了创建实例的方法。 当你需要新的某物,你可以直接 new Thingamabob(),或者随便什么你喜欢的表达法。 类是实例的生产工厂。

不用类,我们怎样创建新的实例? 特别地,我们如何创建一堆有共同点的新东西? 就像这个设计模式,在 Self 中,达到这点的方式是使用克隆。

在 Self 语言中,就好像每个对象都自动支持原型设计模式。 任何对象都能被克隆。为了获得一堆相似的对象,你:

将对象塑造成你想要的状态。你可以直接克隆系统内建的基本Object,然后向其中添加字段和方法。

无需烦扰自己实现 clone();我们就实现了优雅的原型模式,原型被内建在系统中。

JavaScript又怎么样呢?

Brendan Eich,JavaScript 的缔造者, 从 Self语言中直接汲取灵感,很多 JavaScript 的语义都是基于原型的。 每个对象都有属性的集合,包含字段和 “方法”(事实上只是存储为字段的函数)。

A对象可以拥有 B对象,B对象被称为 A对象的“原型”, 如果 A对象的字段获取失败就会委托给 B对象。

但除那以外,我相信在实践中,JavaScript更像是基于类的而不是基于原型的语言。 JavaScript 与 Self 有所偏离,其中一个要点是除去了基于原型语言的核心操作“克隆”。

在 JavaScript 中没有方法来克隆一个对象。 最接近的方法是 Object.create(),允许你创建新对象作为现有对象的委托。 这个方法在 ECMAScript5 中才添加,而那已是 JavaScript 出现后的第十四年了。

相对于克隆,让我带你参观一下 JavaScript 中定义类和创建对象的经典方法。 我们从构造器函数开始:

function Weapon(range, damage) {
this.range = range;
this.damage = damage;
}

这创建了一个新对象,初始化了它的字段。你像这样引入它:

var sword = new Weapon(10, 16);

这里的 new 调用 Weapon() 函数,而 this 绑定在新的空对象上。 函数为新对象添加了一系列字段,然后返回填满的对象。

new 也为你做了另外一件事。 当它创建那个新的空对象时,它将空对象的委托和一个原型对象连接起来。 你可以用 Weapon.prototype 来获得原型对象。

属性是添加到构造器中的,而定义行为通常是通过向原型对象添加方法。就像这样:

Weapon.prototype.attack = function(target) {
if (distanceTo(target) > this.range) {
console.log("Out of range!");
} else {
target.health -= this.damage;
}
}

这给武器原型添加了 attack属性,其值是一个函数。 由于 new Weapon() 返回的每一个对象都有给 Weapon.prototype 的委托, 你现在可以通过调用 sword.attack() 来调用那个函数。 看上去像是这样:

为数据模型(JSON)构建原型

随着编程的进行,如果你比较程序与数据的字节数, 那么你会发现数据的占比稳定地增长。 早期的游戏在程序中生成几乎所有东西,这样程序可以塞进磁盘和老式游戏卡带。 在今日的游戏中,代码只是驱动游戏的 “引擎”,游戏是完全由数据定义的。

这很好,但是将内容推到数据文件中并不能魔术般地解决组织大项目的挑战。 它只能把这挑战变得更难。 我们使用编程语言就因为它们有办法管理复杂性。

不再是将一堆代码拷来拷去,我们将其移入函数中,通过名字调用。 不再是在一堆类之间复制方法,我们将其放入单独的类中,让其他类可以继承或者组合。

当游戏数据达到一定规模时,你真的需要考虑一些相似的方案。 我不指望在这里能说清数据模式这个问题, 但我确实希望提出个思路,让你在游戏中考虑考虑:使用原型和委托来重用数据。

假设我们为早先提到的山寨版《圣铠传说》定义数据模型。 游戏设计者需要在很多文件中设定怪物和物品的属性。

一个常用的方法是使用JSON。 数据实体一般是字典,或者属性集合

{
"name": "goblin grunt",
"minHealth": 20,
"maxHealth": 30,
"resists": ["cold", "poison"],
"weaknesses": ["fire", "light"]
}

你可以给哥布林大家族添加几个兄弟分支:

{
"name": "goblin wizard",
"minHealth": 20,
"maxHealth": 30,
"resists": ["cold", "poison"],
"weaknesses": ["fire", "light"],
"spells": ["fire ball", "lightning bolt"]
}

{
"name": "goblin archer",
"minHealth": 20,
"maxHealth": 30,
"resists": ["cold", "poison"],
"weaknesses": ["fire", "light"],
"attacks": ["short bow"]
}

现在,如果这是代码,我们会闻到了臭味。 在实体间有很多的重复,训练优良的程序员讨厌重复。 它浪费了空间,消耗了作者更多时间。 你需要仔细阅读代码才知道这些数据是不是相同的。 这难以维护。 如果我们决定让所有哥布林变强,需要记得将三个哥布林都更新一遍。糟糕糟糕糟糕。

如果这是代码,我们会为 “哥布林” 构建抽象,并在三个哥布林类型中重用。 但是无能的 JSON 没法这么做。所以让我们把它做得更加巧妙些。

我们可以为对象添加 "prototype" 字段,记录委托对象的名字。 如果在此对象内没找到一个字段,那就去委托对象中查找。

这让"prototype"不再是数据,而成为了元数据。 哥布林有绿色疣皮和黄色牙齿。 它们没有原型。 原型是表示哥布林的数据模型的属性,而不是哥布林本身的属性。

这样,我们可以简化我们的哥布林JSON内容:

{
"name": "goblin grunt",
"minHealth": 20,
"maxHealth": 30,
"resists": ["cold", "poison"],
"weaknesses": ["fire", "light"]
}

{
"name": "goblin wizard",
"prototype": "goblin grunt",
"spells": ["fire ball", "lightning bolt"]
}

{
"name": "goblin archer",
"prototype": "goblin grunt",
"attacks": ["short bow"]
}

由于弓箭手和术士都将 grunt 作为原型,我们就不需要在它们中重复血量,防御和弱点。 我们为数据模型增加的逻辑超级简单——基本的单一委托——但已经成功摆脱了一堆冗余。

有趣的事情是,我们没有更进一步,把哥布林委托的抽象原型设置成 “基本哥布林”。 相反,我们选择了最简单的哥布林,然后委托给它。

在基于原型的系统中,对象可以克隆产生新对象是很自然的, 我认为在这里也一样自然。这特别适合记录那些只有一处不同的实体的数据。

想想 Boss 和其他独特的事物,它们通常是更加常见事物的重新定义, 原型委托是定义它们的好方法。 断头魔剑,就是一把拥有加成的长剑,可以像下面这样表示:

{
"name": "Sword of Head-Detaching",
"prototype": "longsword",
"damageBonus": "20"
}